ICS3U: Introduction to Computer Science, Grade 11, University Preparation

Unit 3: Programming in Java

Activity 3: Methods Part II - Functions and Unit Testing

Learning Goal: By the end of this activity, I will be able to create and validate methods (functions) using parameter passing and appropriate variable scope to perform tasks in programs.

Content


Recall: A method consists of:

The general form of a method is:

<optional_modifiers> <return_type> <name>(<parameters>) {
    <statements>
}

Return Statements

Below is a modification of the showSum(int, int, int) example above, which returns the sum, so main method can output it to the console.

package activity3;

import simpleIO.Console;

public class MethodsExample {

    public static void main(String[] args) {
        int total = calcSum(2, 3, 10);
        Console.print("The sum is " + total);
    }

    public static int calcSum(int a, int b, int c) {
        int sum = a + b + c;
        return sum;
    }
}

In this version of the program, the result of the addition statement is returned to the main method, and assigned to the variable total.

Did you know icon
  • Recall: Any variables declared inside a method have a local scope - they cannot be used or accessed outside of the method.
    • Methods can only send information back to where they are called from with a return statement

Parameters and return statements are like a one-way-street. Information flows either to the method (parameters), or back to the code that called the method (return statement)

The example below shows a method cubeOf(double), which returns the cube of its parameter

Try running this code in Eclipse.

package activity3;

import simpleIO.Console;

public class CubeCalculator {

    public static void main(String[] args) {
        double num, cube;

        // Input
        num = Console.readDouble("Enter a number:");

        // Processing
        cube = cubeOf(num);

        // Output
        Console.print("The cube of " + num + " is " + cube);
    }

    public static double cubeOf(double x) {
        double cube;
        cube = x * x * x;
        return cube;
    }

}

Notice this example includes two methods that return values - the Console.readDouble(String) as well as cubeOf(double). Since cubeOf is defined inside the same class as the method call, you do not need to include the class name. However, the readDouble() method is located inside the Console class, so you must include its class name for the method call to work.

In the previous activity, we created methods that printed directly to the Console. Our goal for this activity is to limit calls to Console from methods and use the Console class only in the main method. One way to do this is to have our methods return a String, and then this value is printed to the Console.

Below is an example of a modified RightTriangle application from the previous activity. Notice that now the method returns a String instead of using Console.print().

package activity3;

import simpleIO.Console;

public class RightTriangle {

  public static void main(String[] args) {

      int triangleSize = Console.readInt("Enter a size for your triangle: ");

      /* draw a right triangle with base size entered by user */
      for (int i = 1; i <= triangleSize; i++) {
          Console.print(createBar(i)); //parameter: i
      }
  }

  public static String createBar(int length) { //argument: length
      String triangle = "";

      for (int i = 0; i < length; i++) {
          triangle += "* ";
      }

      return triangle;
  }
}

One final note about terminology: Methods that return a value are also called functions.

Program Testing

It is important that a programmer validate his/her program using a full range of test cases before it is implemented.  This means that when you are testing your program you should try and see how it will react to a variety of input values and ensure that the results you receive are correct. When creating test cases, you should include both regular inputs, as well as inputs that might cause potential problems in the program.

To manually test a program can be very time consuming, so it can be helpful to use an automated testing strategy to simplify the process.  In this activity we will be learning about the JUnit automated testing framework that is integrated into the Eclipse IDE.

In order for automated testing to be useful, you must use functions in your programs. The JUnit framework is used for testing methods, but it is not meant for testing the main() method. Recall the general model for designing computer programs: Input - Processing - Storage - Output. Ideally, your methods will contain the Processing portion of your program, while the main method, using the Console or Dialog class, will handle the Input & Output. This setup will allow you to make use of automated testing.

Refactoring: Extract Method

Considering the RectangleArea program from Unit 2, Activity 3. You can refactor it to do the processing portion in a method:

Select the code that you wish to place in a method.

Give your method a name, give it the PUBLIC access, and check that the parameters are correct

Eclipse will create the method and replace the code with a method call

Now your method is ready to be tested using JUnit!

Creating Automated Tests

To create an automated test, follow these steps:

  1. In the Package Explorer, select the class you want to test
  2. Select File -> New -> JUnit Test Case
  3. The Name and Class under test fields should be filled out for you, as shown below.
  4. Click Finish when you are done.

Once your JUnit Class is created, you will need to populate it with test cases. For each test case, you must decide what input you will use, as well as the output that you expect to receive. Then call the assertEquals(expected, actual) method provided by the JUnit framework.

The following is a simple automated test for the RectangleArea class. Notice that each test scenario (normal behaviour, one input is zero, one input is negative) is placed in its own method, with one or more test cases per method. Also the annotation "@Test" must be placed prior to each method so JUnit knows how to run it correctly.

package activity3;

import static org.junit.Assert.*;
import org.junit.Test;

public class RectangleAreaTest {

    @Test
    public void testNormalScenario() {
        assertEquals(25, RectangleArea.calculateArea(5, 5));
        assertEquals(42, RectangleArea.calculateArea(6, 7));
    }

    @Test
    public void testZeroValues() {
        assertEquals(0, RectangleArea.calculateArea(0, 5));
        assertEquals(0, RectangleArea.calculateArea(5, 0));
    }

    @Test
    public void testNegativeValues() {
        assertEquals(-1, RectangleArea.calculateArea(-5, 5));
    }

}

When you run a JUnit Test, the results are shown in the JUnit View, usually in the same location as the Console View.

When the test above is run, the third scenario fails, since we have not done any checking for negative inputs. This failed test would be recorded as a bug that needs to be fixed.

There are multiple version of the assertEquals() method provided by the JUnit framework, so you will use the one that is appropriate to the method that you are testing.

For example, you could test the createBar() method in the RightTriangle application like this:

@Test
public void testCreateBar() {
  assertEquals("* * * * * * ", RightTriangle.createBar(6));
  assertEquals("* * ", RightTriangle.createBar(2));
}

You could test the cubeOf() method in the CubeCalculator application like this:

@Test
public void testCube() {
    assertEquals(125.0, CubeCalculator.cubeOf(5.0), 0.5);
    assertEquals(1.728, CubeCalculator.cubeOf(1.2), 0.0005);
}
didyouknow icon

Why is the extra parameter needed when testing methods that return doubles? This is due to the lack of precision in storing double variables, which you may have already noticed in some of your programs.

Usually, the delta argument is a small number that represents the allowable rounding error.

For example, suppose we had a method called calculatePay(int hours, double rate) in an Employee class, which accepts the number of hours worked and the hourly rate, and returns the total pay. The assert statement to test that method could be:

assertEquals(112.50, Employee.calculatePay(15, 7.50), 0.005);

This delta value of 0.005 would allow for rounding (up or down) to the nearest hundredth (2 decimal places of accuracy).

When creating your tests, consider the following suggestions:

Finally, it's important to realize that not all programs can be testing only with automated tests (there will still be a need for some manual testing!), but they are a great asset for designing robust programs. Many programmers adopt a "test-driven development" programming style, where the programmer creates their automated tests before creating their programs. That way they know when their program is complete; it passes all the test cases!

GitHub Actions

Continuous Integration (CI) is a set of tools that allows developers to write and test their code with immediate, automated feedback. All of the remaining activities in Unit 3 come with a series of JUnit tests that are pre-configured to let you know if your code is working.

To access the results of your automated tests, visit your repository on GitHub.com and select "Actions". This will allow you to view the results of the autograding tests that I have created.

Reading and interpreting the results can be tricky, so I've created a short video to show some common types of results.

Below are some images of what the output of the autograding tests could look like:

If the method exists, but is not implemented correctly, you will get a FAILED message.

Pay close attention to these; they indicate that there is a logic error in your code. Return to your code, review the requirements, or ask your teacher to help you diagnose the problem.

If a class or method is missing, you will get an ERROR.

If you have named a class or a method incorrectly, you will get an error as well. Please ensure that your class names and method names are exactly as specified in the practice exercises.

It is normal to see many of these messages at the start of the unit. They should go away as you implement the required classes and methods

If everything works perfectly, you should see a BUILD SUCCESSFUL message and the activity title with a green checkmark.

Each batch of tests is run for an entire activity. You will not see this message until all tests for a particular activity have passed.

Evidence of Learning


Programming Exercises

Once you have read the material above complete the following activities. Place all your classes in the activity3 package. Use the names in bold as your class name / method name for each program!

  1. Create a SpanishNumbers application that displays numbers 1 through 10 in Spanish.
    • Your application must contain the function: public static String translate(int number)
    • Your main method should resemble this:
    • public static void main(String[] args) {
      
          //Display numbers 1 through 10 in Spanish
          for (int i = 1; i <= 10; i++) {
              String spanishNumber = translate(i);
              Console.print(i + " in spanish is " + spanishNumber);
          }
      }
      

    FYI: 1 - uno, 2 - dos, 3 - tres, 4 - cuatro, 5 - cinco, 6 - seis, 7 - siete, 8 - ocho, 9 - nueve, 10 - diez

    Visit GitHub Actions and verify that the junit tests executed properly

  2. Create your OWN SpanishNumbersTest (automated JUnit Tests for the SpanishNumbers program that you created) to test all branches of your decision structure. Run your tests to verify that they work.
  3. Choose ONE program from activity 3, 4 or 5 in Unit 2. Copy this program into your activity 3 package and refactor your program so that the logic is performed in a custom function, instead of the main method.
    ONLY YOUR MAIN METHOD SHOULD CONTAIN CALLS TO CONSOLE OR DIALOG!! Use parameters and return values in your new program.
    Then, create a JUnit test to test your method.

Commit your progress to GitHub every day. Submit the link on the Hapara evidence card when you are finished.